{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "bd82072c-f044-45aa-b2f5-496ed1f2c261",
   "metadata": {},
   "outputs": [],
   "source": [
    "model_name_or_path = \"openai/whisper-large-v2\"\n",
    "model_dir = \"../models/whisper-larg-v2-asr-int8\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "05c23c16-8641-46f4-b7c2-364fb6c2a0e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "language = \"Chinese (China)\"\n",
    "language_addr = \"zh-CN\"\n",
    "task = \"transcribe\"\n",
    "dataset_name = \"mozilla-foundation/common_voice_11_0\"\n",
    "batch_size = 64"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "b2694e73-a524-41e4-aed0-b7c10e43a72e",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading readme: 14.4kB [00:00, 10.3MB/s]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'client_id': '95368aab163e0387e4fd4991b4f2d8ccfbd4364bf656c860230501fd27dcedf087773e4695a6cf5de9c4f1d406d582283190d065cdfa36b0e2b060cffaca977e',\n",
       " 'path': '/home/david/.cache/huggingface/datasets/downloads/extracted/91f69bf474d91e45d0587628c175bc69e6cf6c29698483712c3b06c25075fbcf/zh-CN_train_0/common_voice_zh-CN_33211332.mp3',\n",
       " 'audio': {'path': '/home/david/.cache/huggingface/datasets/downloads/extracted/91f69bf474d91e45d0587628c175bc69e6cf6c29698483712c3b06c25075fbcf/zh-CN_train_0/common_voice_zh-CN_33211332.mp3',\n",
       "  'array': array([-6.82121026e-13, -2.27373675e-12, -2.27373675e-12, ...,\n",
       "          1.21667399e-05,  3.23003678e-06, -2.43066324e-07]),\n",
       "  'sampling_rate': 48000},\n",
       " 'sentence': '性喜温暖润湿气候且耐寒。',\n",
       " 'up_votes': 2,\n",
       " 'down_votes': 0,\n",
       " 'age': '',\n",
       " 'gender': '',\n",
       " 'accent': '',\n",
       " 'locale': 'zh-CN',\n",
       " 'segment': ''}"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from datasets import load_dataset, DatasetDict\n",
    "common_voice = DatasetDict()\n",
    "common_voice[\"train\"] = load_dataset(dataset_name, language_addr, split=\"train\", trust_remote_code=True)\n",
    "common_voice[\"validation\"] = load_dataset(dataset_name, language_addr, split=\"validation\", trust_remote_code=True)\n",
    "common_voice[\"train\"][0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "55f7105b-2e40-4d72-a82e-4911e2e1ddaf",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
      "  warnings.warn(\n",
      "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n",
      "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n"
     ]
    }
   ],
   "source": [
    "from transformers import AutoFeatureExtractor, AutoTokenizer, AutoProcessor\n",
    "\n",
    "feature_extractor = AutoFeatureExtractor.from_pretrained(model_name_or_path)\n",
    "tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, language=language, task=task)\n",
    "processor = AutoProcessor.from_pretrained(model_name_or_path, language=language, task=task)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "ab689aaf-9a5d-49ea-989f-2a32aa107399",
   "metadata": {},
   "outputs": [],
   "source": [
    "ommon_voice = common_voice.remove_columns(\n",
    "    [\"accent\", \"age\", \"client_id\", \"down_votes\", \"gender\", \"locale\", \"path\", \"segment\", \"up_votes\"]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "9abe85a7-7118-414b-9c99-9507a4b9f7d3",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Map: 100%|████████████████████████████████████████████████████████████████| 29056/29056 [55:30<00:00,  8.72 examples/s]\n",
      "Map: 100%|████████████████████████████████████████████████████████████████| 10581/10581 [21:03<00:00,  8.37 examples/s]\n"
     ]
    }
   ],
   "source": [
    "from datasets import Audio\n",
    "\n",
    "common_voice = common_voice.cast_column(\"audio\", Audio(sampling_rate=16000))\n",
    "\n",
    "def prepare_dataset(batch):\n",
    "    audio = batch[\"audio\"]\n",
    "    batch[\"input_features\"] = feature_extractor(audio[\"array\"], sampling_rate=audio[\"sampling_rate\"]).input_features[0]\n",
    "    batch[\"labels\"] = tokenizer(batch[\"sentence\"]).input_ids\n",
    "    return batch\n",
    "    \n",
    "tokenized_common_voice = common_voice.map(prepare_dataset)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3c7d2d8d-4e1b-4deb-b8fb-e7b8dbe2e228",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "\n",
    "from dataclasses import dataclass\n",
    "from typing import Any, Dict, List, Union\n",
    "\n",
    "@dataclass\n",
    "class DataCollatorSpeechSeq2SeqWithPadding:\n",
    "    processor: Any  \n",
    "    def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:\n",
    "\n",
    "        input_features = [{\"input_features\": feature[\"input_features\"]} for feature in features]\n",
    "        batch = self.processor.feature_extractor.pad(input_features, return_tensors=\"pt\")\n",
    "\n",
    "        label_features = [{\"input_ids\": feature[\"labels\"]} for feature in features]\n",
    "        labels_batch = self.processor.tokenizer.pad(label_features, return_tensors=\"pt\")\n",
    "\n",
    "        labels = labels_batch[\"input_ids\"].masked_fill(labels_batch.attention_mask.ne(1), -100)\n",
    "\n",
    "        if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().cpu().item():\n",
    "            labels = labels[:, 1:]\n",
    "\n",
    "        batch[\"labels\"] = labels\n",
    "\n",
    "        return batch  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "fafb4549-39cd-4530-9501-55c3b1701298",
   "metadata": {},
   "outputs": [],
   "source": [
    "data_collator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "b7760ac7-2219-4ced-85c9-e3346d83f502",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "from transformers import AutoModelForSpeechSeq2Seq\n",
    "\n",
    "model = AutoModelForSpeechSeq2Seq.from_pretrained(model_name_or_path, load_in_8bit=True, device_map=\"auto\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "e1ba3a9d-030d-4034-9282-661533b4e596",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.config.forced_decoder_ids = None\n",
    "model.config.suppress_tokens = []"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "47bd6bdf-2e2d-4f9c-b4f2-3c519d11b84d",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/peft/utils/other.py:141: FutureWarning: prepare_model_for_int8_training is deprecated and will be removed in a future version. Use prepare_model_for_kbit_training instead.\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "from peft import prepare_model_for_int8_training\n",
    "\n",
    "model = prepare_model_for_int8_training(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "117398ed-b019-4307-b7bc-d14886793ad7",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = prepare_model_for_int8_training(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "05a204fb-df57-4c6b-ba69-27c2680e8677",
   "metadata": {},
   "outputs": [],
   "source": [
    "from peft import LoraConfig, PeftModel, LoraModel, LoraConfig, get_peft_model\n",
    "\n",
    "config = LoraConfig(\n",
    "    r=4,  \n",
    "    lora_alpha=64,  \n",
    "    target_modules=[\"q_proj\", \"v_proj\"],\n",
    "    lora_dropout=0.05,  \n",
    "    bias=\"none\", \n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "b97a543c-3fe8-4e7d-98c6-861ad97fc5f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "peft_model = get_peft_model(model, config)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "b32df83f-2f0a-48d8-93c0-ea909cf4fc47",
   "metadata": {},
   "outputs": [],
   "source": [
    "from transformers import Seq2SeqTrainingArguments\n",
    "\n",
    "training_args = Seq2SeqTrainingArguments(\n",
    "    output_dir=model_dir,  \n",
    "    per_device_train_batch_size=batch_size,  \n",
    "    learning_rate=1e-3,  \n",
    "    num_train_epochs=3,  \n",
    "    evaluation_strategy=\"epoch\",  \n",
    "    warmup_steps=50,  \n",
    "    fp16=True,  \n",
    "    per_device_eval_batch_size=batch_size,  \n",
    "    generation_max_length=128,  \n",
    "    logging_steps=10,  \n",
    "    remove_unused_columns=False,  \n",
    "    label_names=[\"labels\"],  \n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "607973ee-39f9-47f7-839d-8b787a54e245",
   "metadata": {},
   "outputs": [],
   "source": [
    "from transformers import Seq2SeqTrainer\n",
    "\n",
    "trainer = Seq2SeqTrainer(\n",
    "    args=training_args,\n",
    "    model=peft_model,\n",
    "    train_dataset=tokenized_common_voice[\"train\"],\n",
    "    eval_dataset=tokenized_common_voice[\"validation\"],\n",
    "    data_collator=data_collator,\n",
    "    tokenizer=processor.feature_extractor,\n",
    ")\n",
    "peft_model.config.use_cache = False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d2374b07-7c13-4611-9fb9-a3df8cb29ce0",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/torch/utils/checkpoint.py:464: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. In version 2.4 we will raise an exception if use_reentrant is not passed. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/torch/utils/checkpoint.py:91: UserWarning: None of the inputs have requires_grad=True. Gradients will be None\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/bitsandbytes/autograd/_functions.py:322: UserWarning: MatMul8bitLt: inputs will be cast from torch.float32 to float16 during quantization\n",
      "  warnings.warn(f\"MatMul8bitLt: inputs will be cast from {A.dtype} to float16 during quantization\")\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    <div>\n",
       "      \n",
       "      <progress value='1362' max='1362' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
       "      [1362/1362 19:39:40, Epoch 3/3]\n",
       "    </div>\n",
       "    <table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       " <tr style=\"text-align: left;\">\n",
       "      <th>Epoch</th>\n",
       "      <th>Training Loss</th>\n",
       "      <th>Validation Loss</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <td>1</td>\n",
       "      <td>0.339500</td>\n",
       "      <td>0.400262</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <td>2</td>\n",
       "      <td>0.312400</td>\n",
       "      <td>0.384237</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <td>3</td>\n",
       "      <td>0.244700</td>\n",
       "      <td>0.387206</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table><p>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/torch/utils/checkpoint.py:464: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. In version 2.4 we will raise an exception if use_reentrant is not passed. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/torch/utils/checkpoint.py:91: UserWarning: None of the inputs have requires_grad=True. Gradients will be None\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/bitsandbytes/autograd/_functions.py:322: UserWarning: MatMul8bitLt: inputs will be cast from torch.float32 to float16 during quantization\n",
      "  warnings.warn(f\"MatMul8bitLt: inputs will be cast from {A.dtype} to float16 during quantization\")\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/torch/utils/checkpoint.py:464: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. In version 2.4 we will raise an exception if use_reentrant is not passed. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/torch/utils/checkpoint.py:91: UserWarning: None of the inputs have requires_grad=True. Gradients will be None\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/bitsandbytes/autograd/_functions.py:322: UserWarning: MatMul8bitLt: inputs will be cast from torch.float32 to float16 during quantization\n",
      "  warnings.warn(f\"MatMul8bitLt: inputs will be cast from {A.dtype} to float16 during quantization\")\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "TrainOutput(global_step=1362, training_loss=0.3346679742815911, metrics={'train_runtime': 70830.6344, 'train_samples_per_second': 1.231, 'train_steps_per_second': 0.019, 'total_flos': 1.85319357677568e+20, 'train_loss': 0.3346679742815911, 'epoch': 3.0})"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "trainer.train()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "6df44588-2af4-4916-8570-789abf1dc3ac",
   "metadata": {},
   "outputs": [],
   "source": [
    "trainer.save_model(model_dir)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c8b83e1c-ad22-456f-b8cd-6904562a90b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "model_dir = \"../models/whisper-large-v2-asr-int8\"\n",
    "language = \"Chinese(China)\"\n",
    "langguage_addr = \"zh-CN\"\n",
    "language_decode = \"chinese\"\n",
    "task = \"transcribe\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "342089a6-53f6-4f1b-9197-8e59026e7ffb",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
      "  warnings.warn(\n",
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/huggingface_hub/file_download.py:797: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "from transformers import AutoModelForSpeechSeq2Seq, AutoTokenizer, AutoProcessor\n",
    "from peft import PeftConfig, PeftModel\n",
    "\n",
    "peft_config = PeftConfig.from_pretrained(model_dir)\n",
    "\n",
    "base_model = AutoModelForSpeechSeq2Seq.from_pretrained(\n",
    "    peft_config.base_model_name_or_path, load_in_8bit=True, device_map=\"auto\"\n",
    ")\n",
    "\n",
    "peft_model = PeftModel.from_pretrained(base_model, model_dir)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "0e0eb473-53ea-4e6f-9755-314ad77cc09e",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n",
      "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n"
     ]
    }
   ],
   "source": [
    "tokenizer = AutoTokenizer.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)\n",
    "processor = AutoProcessor.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)\n",
    "feature_extractor = processor.feature_extractor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "92d87a56-1ac4-45e5-8245-4a72c1dfb333",
   "metadata": {},
   "outputs": [],
   "source": [
    "test_audio = \"../LLM-quickstart/peft/data/audio/test_zh.flac\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "4d5f8541-fdb6-47aa-ac50-8aa5c00358e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from transformers import AutomaticSpeechRecognitionPipeline\n",
    "\n",
    "pipeline = AutomaticSpeechRecognitionPipeline(model=peft_model, tokenizer=tokenizer, feature_extractor=feature_extractor)\n",
    "\n",
    "forced_decoder_ids = processor.get_decoder_prompt_ids(language=language_decode, task=task)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "215c45a6-1b1d-46e9-baf9-915ecd53b972",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/david/anaconda3/envs/peft/lib/python3.10/site-packages/bitsandbytes/autograd/_functions.py:322: UserWarning: MatMul8bitLt: inputs will be cast from torch.float32 to float16 during quantization\n",
      "  warnings.warn(f\"MatMul8bitLt: inputs will be cast from {A.dtype} to float16 during quantization\")\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "\n",
    "with torch.cuda.amp.autocast():\n",
    "    text = pipeline(test_audio, max_new_tokens=255)[\"text\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "efbc1149-2ffb-44ea-8300-fd21ca399435",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'这是一段测试用于Whisper Large V2模型的自动语音识别测试。'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "text"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f98aa393-e827-49e1-a546-f007e4853321",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Object `pipline` not found.\n"
     ]
    }
   ],
   "source": [
    "??pipline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "79c96eac-14c1-4849-98d9-20eec767a706",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
